﻿//////////////////////////////////////////////////////////////////
//
// TerrainSRTM\Terrain.cs
//
// Copyright (c) 2012 Dan Pike.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation files
// (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using Interfaces;
using Ionic.Zip;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Utilities;
using System.IO;
using System.Diagnostics;

namespace TerrainSRTM
{
   public class Terrain : ITerrain
   {
      private static Logger logger_;
      private static readonly string[] heightFileCheckOrder_;
      private string planetName_;
      
      /// <summary>
      /// The cache of height tiles
      /// </summary>
      private int cacheSize_;
      private Dictionary<ILocation, short[,]> cache_;
      private Dictionary<ILocation, DateTime> downloading_;
      private SortedList<long, ILocation> lruDates_;

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      static Terrain()
      {
         // Precedence for the filenames that we should use
         heightFileCheckOrder_ = new string[] { ".compiled", ".hgt", ".hgt.zip" };
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public Terrain()
      {
         logger_ = Program.Logger;

         // Each cache entry is one arc minute of height points, which is
         // 3600 points for tiles that are taken from SRTM1. A cache size
         // of 2048 means that we spend about 7 MBytes on caching tiles,
         // but allows for a display showing 2700x2700 points approximately
         cacheSize_ = 2048;
         cache_ = new Dictionary<ILocation, short[,]>(cacheSize_);
         downloading_ = new Dictionary<ILocation, DateTime>();
         lruDates_ = new SortedList<long, ILocation>(cacheSize_);
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ITerrain
      /// </summary>
      public bool Initialize(string planetName)
      {
         try
         {
            planetName_ = planetName;
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ITerrain
      /// </summary>
      public short[,] LoadTile(ILocation location)
      {
         try
         {
            string srtmFilename = GetFilename(location);
            if (!File.Exists(srtmFilename))
            {
               // Output a warning but don't throw any exception; the system
               // will just display blue for the sea
               logger_.WarnFormat("there is no tile for '{0}' (file '{1}' does not exist)",
                  location, srtmFilename);
               return null;
            }

            // Each tile covers one arc minute (60 arc seconds), so we should
            // use a version of the supplied location that has no seconds component
            short[,] heights;
            ILocation tileLocation = location.Clone();
            tileLocation.LatitudeSeconds = tileLocation.LongitudeSeconds = 0;
            tileLocation.Altitude = 0;

            bool inCache = false;
            lock (this)
            {
               inCache = cache_.TryGetValue(tileLocation, out heights);
            }
            if (!inCache)
            {
               //logger_.debugFormat("loading tile '{0}' from file '{1}'", tileLocation, srtmFilename);

               // For a 60x60 tile, we need 61x61 points
               // The SRTM file is an array of 3602x3602 points, going from north-south, west-east
               // The heights array is an array of 61x61 points, going from south-north, west-east

               heights = new short[Const.tileWidth61, Const.tileWidth61];
               using (FileStream stream = new FileStream(srtmFilename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
               {
                  BinaryReader reader = new BinaryReader(stream);

                  // Calculate the offset within the file of the cell for the north-west corner of the tile.
                  int rowOffset, columnOffset;
                  if (Hemisphere.North == tileLocation.LatitudeHemisphere)
                  {
                     rowOffset = (59 - tileLocation.LatitudeMinutes) * Const.tileWidth60 + 1;
                  }
                  else
                  {
                     // Fill this in when we get the algo sorted
                     throw new NotImplementedException();
                  }

                  // Do the same thing for the longitude
                  if (Hemisphere.East == tileLocation.LongitudeHemisphere)
                  {
                     // Fill this in when we get the algo sorted
                     throw new NotImplementedException();
                  }
                  else
                  {
                     columnOffset = (59 - tileLocation.LongitudeMinutes) * Const.tileWidth60 + 1;
                  }

                  // Combine into a file offset and multiple by 2 because each point is 16-bits
                  int fileOffset = (Const.srtmRow3602 * rowOffset + columnOffset) * 2;

                  // Count the rows down from the max, as the file data and height array
                  // are laid out in opposite senses in the north <-> south orientation
                  for (int row = Const.tileWidth60; row >= 0; --row)
                  {
                     // Jump to the start of the row
                     if (stream.Seek(fileOffset, SeekOrigin.Begin) != fileOffset)
                     {
                        throw new InvalidOperationException("overflowed the height file");
                     }

                     // but count the columns the same way as both go west -> east
                     for (int column = 0; column <= Const.tileWidth60; ++column)
                     {
                        heights[row, column] = reader.ReadInt16();
                     }

                     // Adjust the fileOffset to point at the start of the next row
                     fileOffset += Const.srtmRow3602 * 2;
                  }
               }

               lock (this)
               {
                  // Make space in the cache, if we need to
                  while (cacheSize_ < lruDates_.Count)
                  {
                     ILocation dropCache = lruDates_.Values[0];
                     logger_.DebugFormat("dropping location '{0}' from the tile cache", dropCache);
                     cache_.Remove(dropCache);
                     lruDates_.RemoveAt(0);
                  }

                  // Put this new tile in the cache
                  cache_.Add(tileLocation, heights);
               }
            }
            return heights;
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return null;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ITerrain
      /// </summary>
      public void TileImage(ILocation location, short[,] heights)
      {
         try
         {
            lock (this)
            {
               downloading_.Remove(location);
               cache_[location] = heights;
            }
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ITerrain
      /// </summary>
      public short[,] DownloadTile(ILocation location)
      {
         short[,] heights = null;
         try
         {
            ILocation tileLocation = location.Clone();
            tileLocation.LatitudeSeconds = tileLocation.LongitudeSeconds = 0;
            tileLocation.Altitude = 0;

            lock (this)
            {
               // If we have the value cached, then return it ...
               if (cache_.TryGetValue(tileLocation, out heights))
               {
                  return heights;
               }

               // ... if we requested it a short time ago, then carry on
               // waiting ...
               DateTime requestSent, now = DateTime.Now;
               if (downloading_.TryGetValue(tileLocation, out requestSent) &&
                  (5.0 > (now - requestSent).TotalSeconds))
               {
                  // Wait for the reply
                  return null;
               }

               // ... otherwise send (another) request for the tile
               downloading_[tileLocation] = now;
            }

            Program.Transport.LoadTile(planetName_, location);
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return heights;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Build an SRTM-filename root from an ILocation
      /// </summary>
      private string GetFilename(ILocation address)
      {
         StringBuilder sb = new StringBuilder();

         sb.Append(address.LatitudeHemisphere.ToString().Substring(0, 1));
         if (Hemisphere.South == address.LatitudeHemisphere)
         {
            // Sort this out when we have the algo working
            throw new NotImplementedException();
         }
         sb.Append(address.LatitudeDegrees);

         sb.Append(address.LongitudeHemisphere.ToString().Substring(0, 1));
         if (Hemisphere.East == address.LatitudeHemisphere)
         {
            // Sort this out when we have the algo working
            throw new NotImplementedException();
         }
         else
         {
            // If we are in the western hemisphere, then the desired file will be
            // the tile to the west of the one given by the longitudeDegrees value
            // since that specifies the south-west corner of the file and we will
            // want to be even further west than that
            sb.Append((address.LongitudeDegrees - 1).ToString("000"));
         }
         sb.Append(".swapped");
         return Path.Combine(Program.Settings.TileFolder, sb.ToString());
      }
   }
}
